import { readFile } from "fs/promises"
import { resolve } from "path"
import { h } from "koishi"
import seedrandom from "seedrandom"

export const name = "yscarplate"
export const inject = ["canvas"]

export async function apply(ctx) {
  const fontFileNames = [
    "IosevkaCustom-SemiCondensedBold.woff2",
    null, // was sarasa mono but we dont sideload that anymore
    "Italianplates1999-mod.woff2",
    "CODE2000.subset.woff2",
    "FZYTJW.subset.woff2",
  ]
  const fontDataURLs = fontFileNames.map(
    async name => name == null ? undefined : "data:font/woff2;base64," + await readFile(resolve("assets/yscarplate", name), "base64")
  )

  function ozero(s) {
    return s.replaceAll("0", "O")
  }

  //function cssEscape(r){var t,e=String(r),n=e.length,a=-1,o="",h=e.charCodeAt(0);if(1==n&&45==h)return"\\"+e;for(;++a<n;)0!=(t=e.charCodeAt(a))?o+=t>=1&&t<=31||127==t||0==a&&t>=48&&t<=57||1==a&&t>=48&&t<=57&&45==h||t==34||t==38||t==39||t==60||t==62?"\\"+t.toString(16)+" ":t>=128||45==t||95==t||t>=48&&t<=57||t>=65&&t<=90||t>=97&&t<=122?e.charAt(a):"\\"+e.charAt(a):o+=" ";return o}

  const booleanFeatures = ["small", "electric", "wheelchair", "autopilot", "antique"]
  const oldPlateRejectedFeatures = ["electric", "wheelchair", "autopilot", "antique"]
  const mutuallyExclusiveFeatureGroups = [["electric", "autopilot", "antique"]]

  function parse(num, color, features = {}) {
    function assertFeatures(props) {
      for (const feature in props) {
        if (props[feature] == null) continue
        if (feature in features && !!features[feature] !== !!props[feature]) throw `错误：此车牌号不可${"禁启"[+!!features[feature]]}用 ${feature} 属性`
      }
    }
    function checkFeatures(props) {
      if ("color" in props && !props.color) {
        warnings.push("车牌号非法或识别失败")
        props.color = "B"
      } else {
        for (const feature in props) {
          if (props[feature] == null) continue
          if (feature === "color") checkColor(props[feature])
          else if (feature in features && !!features[feature] !== !!props[feature]) warnings.push(`此车牌号不应${"禁启"[+!!features[feature]]}用 ${feature} 属性`)
        }
      }
      return props
    }
    function pickColor(norm, candidates) {
      if (color) {
        if (!candidates.includes(color)) warnings.push("此车牌号不应为此颜色，可以为：" + Array.from(candidates).join(""))
        return { color }
      }
      if (norm) return { color: candidates[~~(new seedrandom(norm).quick() * candidates.length)] }
    }
    function checkColor(expected) {
      if (color && color !== expected) warnings.push("此车牌号不应为此颜色，应为 " + expected)
      return { color: color || expected }
    }

    const warnings = []
    function r(num, ...infoParts) {
      const info = Object.assign({}, ...infoParts)
      if (warnings.length) info.warnings = warnings
      const res = { num, ...info, ...features, ...(color ? { color } : null) }
      if (res.old) {
        for (const feature of oldPlateRejectedFeatures)
          if (res[feature]) throw `错误：旧版车牌不可启用 ${feature} 属性`
      }
      for (const group of mutuallyExclusiveFeatureGroups) {
        const enabled = group.filter(feature => res[feature])
        if (enabled.length > 1) throw `错误：${enabled.join("、")} 属性不可共存`
      }
      return res
    }

    num = String(num).trim().replace(/[-_·.\s]+/g, "-").toUpperCase()
    let match
    const isNil = !num.match(/[1-9]/)
    if ((match = num.match(/^试-?(0\d{3})$/))) return r(
      '<div class="try-wrap try-long-wrap"><div class="try try-long">试</div></div>' + ozero(match[1]),
      assertFeatures({ small: false }),
      checkFeatures({
        color: !isNil && "b",
        electric: false,
        wheelchair: false,
        antique: false,
        autopilot: true,
      }),
    )
    if ((match = num.match(/^试-?([CD])-?([1-9]\d{3})$/))) return r(
      '<div class="try-wrap"><div class="try">试</div></div>' + match[1] + '·' + ozero(match[2]),
      assertFeatures({ small: false }),
      checkFeatures({
        color: !isNil && { C: "R", D: "r" }[match[1]] || false,
        electric: false,
        wheelchair: false,
        antique: false,
        autopilot: false,
      }),
    )
    if ((match = num.match(/^([A-Z]{2})-?(\d{5})$/))) return r(
      match[1] + "·" + ozero(match[2]),
      checkFeatures({
        color: !isNil && !match[1].match(/[AEIOUY]/) && "g",
        small: true,
        electric: false,
        wheelchair: false,
        antique: false,
        autopilot: false,
      }),
    )
    if ((match = num.match(/^([A-Z]{3})-?(\d{4})$/))) return r(
      match[1] + "·" + ozero(match[2]),
      checkFeatures({
        color: "k",
        small: false,
        electric: match[1][0] === "D",
        wheelchair: false,
        antique: false,
        autopilot: false,
        ...(match[1].match(/[AEIOUY]/) || isNil
            ? {
              color: false,
              small: !!match[1].match(/^KT[N-Z]|^D?[OPR-Y]|^Q[I-Z]|^DQ|^D?Z[A-R]/),
              electric: false,
            }
            : match[1].match(/^D?J/)
            ? { color: "Y" }
            : match[1].match(/^D?K[B-H]|^Q[B-C]/)
            ? { color: "g" }
            : match[1].match(/^D?K[J-R]|^Q[D-H]/)
            ? { color: "G" }
            : match[1].match(/^D?K[V-Z]/)
            ? { color: "R" }
            : match[1].match(/^KS|^DK[ST]/)
            ? { color: "r" }
            : match[1].match(/^KT[B-M]/)
            ? { color: "N", antique: true }
            : match[1].match(/^KT[N-Z]/)
            ? { color: "N", small: true, antique: true }
            : match[1].match(/^DQ[B-H]/)
            ? { color: "r", small: true }
            : match[1].match(/^D?Q[J-Z]|^D[M-N]/)
            ? { color: "G", small: true }
            : match[1].match(/^D?[PR-X]|^DZ[B-N]/)
            ? { small: true }
            : match[1].match(/^Z[B-J]|^DZ[PQ]/)
            ? { color: "Y", small: true }
            : match[1].match(/^Z[K-R]|^DZR/)
            ? { color: "R", small: true }
            : match[1].match(/^D?Z[S-Z]/)
            ? { wheelchair: true }
            : match[1].match(/^[KQ]/)
            ? { color: false }
            : {}
        ),
      }),
    )
    if (
      // Require separator for ambiguous numbers
      !num.match(/^([2-9])\1\d\d([2-9])\2$/) &&
      (match =
        num.match(/^(?<left>(?<ldigit>[2-9])\k<ldigit>|(?!\d\d)[\dA-HJ-NP-Z]{2})-?(?<right>\d{4})$/) ||
        num.match(/^(?<left>\d{4})-?(?<right>(?<rdigit>[2-9])\k<rdigit>|(?!\d\d)[\dA-HJ-NP-Z]{2})(?<flipped>)$/)
      )
    ) {
      const { left, right } = match.groups
      const flipped = !!match.groups && "flipped" in match.groups
      const [reg, digit] = flipped ? [right, left] : [left, right]
      const isNil = !digit.match(/[1-9]/)
      const farm = !!reg.match(/[01]/)
      return r(
        '<div class="edge"> </div>' +
          [left, right]
            .map(p => p.split("").map(d => `<div class="middle">${d}</div>`).join(""))
            .join('<div class="dot">·</div>') +
          '<div class="edge"> </div>',
        checkFeatures({
          color: !isNil && (farm ? "g" : "k"),
          old: true,
          small: farm,
        }),
      )
    }
    if (match =
      num.match(/^([2-9A-HJ-NP-Z]{2}[A-HJ-NP-Z])-?(\d{3})$/) ||
      num.match(/^(\d{3})-?([A-HJ-NP-Z]{3})(?<flipped>)$/)
    ) {
      const flipped = !!match.groups && "flipped" in match.groups
      const isNil = !(flipped ? match[1] : match[2]).match(/[1-9]/)
      return r(
        '<div class="edge"> </div>' +
          [match[1], match[2]]
            .map(p => p.split("").map(d => `<div class="middle">${d}</div>`).join(""))
            .join('<div class="dot">·</div>') +
          '<div class="edge"> </div>',
        checkFeatures({
          color: !isNil && (
            flipped
              ? match[2].match(/^[AD]/)
                ? "Y"
                : match[2].match(/^Q/)
                ? "G"
                : match[2].match(/^X/)
                ? "r"
                : "k"
              : match[1] > "N"
              ? "G"
              : "k"
          ),
          old: true,
          small: true,
        }),
      )
    }
    if ((match = num.match(/^试-?([1-9]\d{3})$/))) {
      return r(
        '<div class="edge"> </div><div class="try">试</div><div class="dot"> </div>' +
          match[1].split("").map(d => `<div class="middle">${d}</div>`).join("") +
          '<div class="edge"> </div>',
        checkFeatures({
          color: "b",
          old: true,
          small: false,
        }),
      )
    }
    if ((match = num.match(/^试-?([1-9]\d{4})$/))) {
      return r(
        '<div class="try">试</div>' +
          match[1]
            .split("")
            .map(d => `<div class="middle">${d}</div>`)
            .join(""),
        checkFeatures({
          color: "b",
          old: true,
          small: true,
        }),
      )
    }
    if ((match = num.match(/^械-?([A-HJ-NP-Z]{2})-?(\d\d)$/))) {
      const isNil = !match[2].match(/[1-9]/)
      return r(
        '<div class="edge"> </div><div class="try">械</div>' +
          match.slice(1).map(h => h.split("").map(d => `<div class="middle">${d}</div>`).join("")).join('<div class="dot">·</div>') +
          '<div class="edge"> </div>',
        checkFeatures({
          color: !isNil && (match[1] < "K" ? "k" : match[1] < "Z" ? "Y" : "R"),
          old: true,
          small: false,
        }),
      )
    }
    if (match =
      num.match(/^((?!\d\d)[2-9A-HJ-NP-Z]{2})-?(\d{3})$/) ||
      num.match(/^(\d{3})-?((?!\d\d)[2-9A-HJ-NP-Z]{2})(?<flipped>)$/)
    ) {
      const flipped = !!match.groups && "flipped" in match.groups
      const reg = flipped ? match[2] : match[1]
      const digit = flipped ? match[1] : match[2]
      const isNil = !digit.match(/[1-9]/)
      const col = reg.match(/^([A-Z])\1$/) ? "Rr" : reg.match(/\d/) ? "RrGgY" : "GgY"
      return r(
        '<div class="edge"> </div>' +
          [match[1], match[2]]
            .map(p => p.split("").map(d => `<div class="middle5">${d}</div>`).join(""))
            .join('<div class="dot5">·</div>') +
          '<div class="edge"> </div>',
        checkFeatures({
          color: !isNil && (
            reg.match(/^([A-Z])\1$/)
              ? digit.match(/^0/)
                ? "r"
                : "R"
              : reg.match(/^[A-G][A-Z]|^H[A-G]/)
              ? "g"
              : reg.match(/[2-5]|^H[J-Z]|^[J-Q][A-Z]|^R[A-Q]/)
              ? "G"
              : "Y"
          ),
          old: true,
          small: false,
        }),
      )
    }
    if (match =
      num.match(/^((?!\d\d)[2-9A-HJ-NP-Z]{2})-?(\d{2})$/) ||
      num.match(/^(\d{2})-?((?!\d\d)[2-9A-HJ-NP-Z]{2})(?<flipped>)$/)
    ) {
      const flipped = !!match.groups && "flipped" in match.groups
      const norm = match[1] + "-" + match[2]
      const reg = flipped ? match[2] : match[1]
      const digit = flipped ? match[1] : match[2]
      const isNil = !digit.match(/[1-9]/)
      const business = !!reg.match(/[4-9]|^[G-Z][A-Z]$/)
      let p
      if (isNil) {
        p = checkFeatures({ color: false })
      } else if (match[1].match(/[A-Z][A-Z]/)) {
        if (color === "R" || features.small) {
          p = checkFeatures({ color: "R", small: true, electric: false })
        } else if (!("small" in features) && !features.electric) {
          // 未指定牌大小，随机
          const { color } = pickColor(norm, business ? "GR" : "gR")
          p = { color, small: color === "R" }
        }
      }
      p ??= checkFeatures({ color: business ? "G" : "g", small: false })
      return r(
        '<div class="edgelong"> </div>' +
          [match[1], match[2]]
            .map(p => p.split("").map(d => `<div class="middle4">${d}</div>`).join(""))
            .join('<div class="dot4">·</div>') +
          '<div class="edgelong"> </div>',
        p,
        checkFeatures({
          old: true,
        }),
      )
    }
  }

  ctx.command("yscarplate <num>", "元素国车牌生成器", { showWarning: true, checkUnknown: true })
    .option("color", '-c <[kRrGgBbYN]> 强制指定颜色（小写=白底＿字，大写=＿底黑/白字）')
    .option("small", "-s 小型牌照")
    .option("small", "-S 普通大小牌照", { value: false })
    .option("feature", "-f <electric|wheelchair|autopilot|antique> 其他属性（多个用逗号分隔，“no”加属性名以禁用属性）")
    .option("zoom", "-z <multiplier:posint> 缩放倍率", { fallback: 2 })
    .option("backdrop", "-b <color:string> 背景颜色")
    .option("formats", "显示车牌号格式说明", { notUsage: true })
    .option("debug", "输出 HTML 源代码", { hidden: true })
    .action(async ({ options: { color, small: smallOpt, feature: featureOpt, zoom, backdrop, formats, debug } }, ...numParts) => {
      if (formats) return `
        （n表示数字0到9，a表示除I O外的字母，b表示数字0或1，x表示除I O外的字母或除0 1外的数字，所有n不能同时取0）
        xx-nnnn，nnnn-xx（两个x都取数字时必须相同，如0567-33）；
        bx-nnnn，xb-nnnn，nnnn-bx，nnnn-xb；
        xx-nnn，nnn-xx，xx-nn，nn-xx（两个x不能都取数字）；
        xxa-nnn，nnn-aaa；
        aaa-nnnn，aa-nnnnn（a不能取A E I O U Y）；
        试nnnn；
        试nnnnn，试C-nnnn，试D-nnnn（第一位n不能取0）；
      `.trim().replace(/^\s+/gm, "")
      let num = numParts.join(" ")
      const features = Object.create(null)
      if (featureOpt) {
        for (const featureName of featureOpt.trim().toLowerCase().split(",")) {
          if (booleanFeatures.includes(featureName)) features[featureName] = true
          else if (featureName.startsWith("no") && booleanFeatures.includes(featureName.slice(2)))
            features[featureName.slice(2)] = false
          else return `无效属性：${h.escape(featureName)}`
        }
      }
      if (!("small" in features) && smallOpt != null) features.small = smallOpt
      ctx.logger.debug("input", { num, color, ...features })
      let info
      try {
        info = parse(num, color, features)
      } catch (e) {
        if (typeof e === "string") {
          ctx.logger.debug("parse error", e)
          return h.text(e)
        }
        throw e
      }
      ctx.logger.debug("parse result", info)
      if (!info) return "车牌号无效…使用 yscp --formats 查看元素国车牌号格式。"

      const [backgroundColor, textColor] = {
        k: ["white", "black"],
        R: ["red", "white"],
        r: ["white", "red"],
        G: ["green", "white"],
        g: ["white", "green"],
        B: ["dodgerblue", "white"],
        b: ["white", "dodgerblue"],
        Y: ["yellow", "black"],
        N: ["brown", "white"],
      }[info.color] || []
      if (backgroundColor === undefined) return "颜色无效…"

      const [width, height, holesX, holeY, holeWidth, holeHeight] = {
        "": [400, 160, [90, 280], 15, 30, 10],
        "small": [250, 100, [44, 184], 7, 22, 8],
        "old": [320, 160, [50, 240], 15, 30, 10],
        "old_small": [200, 100, [19, 159], 7, 22, 8],
      }[[info.old && "old", info.small && "small"].filter(Boolean).join("_")]
      const borderRadius = 8
      const backgroundPath = [
        ["M", borderRadius, 0],
        ["A", borderRadius, borderRadius, 0, 0, 0, 0, borderRadius],
        ["V", height - borderRadius],
        ["A", borderRadius, borderRadius, 0, 0, 0, borderRadius, height],
        ["H", width - borderRadius],
        ["A", borderRadius, borderRadius, 0, 0, 0, width, height - borderRadius],
        ["V", borderRadius],
        ["A", borderRadius, borderRadius, 0, 0, 0, width - borderRadius, 0],
        ["Z"],
        ...holesX.flatMap(holeX => [
          ["M", holeX + holeHeight / 2, holeY + holeHeight],
          ["A", holeHeight / 2, holeHeight / 2, 0, 0, 1, holeX + holeHeight / 2, holeY],
          ["H", holeX + holeWidth - holeHeight / 2],
          ["A", holeHeight / 2, holeHeight / 2, 0, 0, 1, holeX + holeWidth - holeHeight / 2, holeY + holeHeight],
          ["Z"],
        ])
      ].map(l => l[0] + l.slice(1).join(",")).join("")

      let r = /*xml*/`
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">
          <style>
            @font-face{font-family:"Iosevka Custom";font-stretch:semi-condensed;font-weight:bold;src:url("@@_FONT0_@@")}
            @font-face{font-family:"Sarasa Mono SC";src:local('Sarasa Term SC')}
            @font-face{font-family:"Italian plates 1999";src:url("@@_FONT2_@@")}
            @font-face{font-family:"Code2000";src:url("@@_FONT3_@@")}
            @font-face{font-family:"方正姚体";src:url("@@_FONT4_@@")}
            .try{width:112px;height:100%;font-family:'Sarasa Mono SC';font-stretch:normal;font-weight:400;font-size:88px;line-height:160px;letter-spacing:0}
            .try-long{width:192px;letter-spacing:0}
            .plate:not(.plate-old) .try{position:absolute;left:0;top:0}
            .plate-small .try{width:70px;font-size:55px;line-height:100px}
            .try-wrap{display:inline}
            .try-wrap::after{content:"  ";white-space:pre}
            .try-long-wrap::after{content:"    "}
            .plate{position:relative;width:396.8px;height:160px;font-family:'Iosevka Custom';font-stretch:semi-condensed;font-weight:700;line-height:160px;font-size:112px;letter-spacing:-3.2px;padding-right:3.2px;text-align:center;font-optical-sizing:none}
            .plate-small{width:248px;height:100px;border-radius:5px;line-height:100px;font-size:70px;letter-spacing:-2px;padding-right:2px}
            .chair{position:absolute;left:0;top:0;z-index:100;width:100%;height:40px;font-family:'Code2000';font-stretch:normal;font-weight:normal;font-size:32px;line-height:40px;color:blue;letter-spacing:normal}
            .elec{position:absolute;left:0;top:0;z-index:100;width:100%;height:40px;font-family:'Sarasa Mono SC';font-weight:normal;font-size:24px;line-height:40px;letter-spacing:normal}
            .plate-small .elec{width:250px;height:25px;font-size:15px;line-height:25px}
            .plate-old{width:320px;height:160px;font-family:'Italian plates 1999';display:flex;font-weight:normal;padding:0;letter-spacing:normal}
            .plate-old.plate-small{width:200px;height:100px;padding:0}
            .plate-old .edge{width:8px}
            .plate-old.plate-small .edge{width:5px}
            .plate-old .middle{width:48px}
            .plate-old.plate-small .middle{width:30px}
            .plate-old .dot{width:16px}
            .plate-old.plate-small .dot{width:10px}
            .plate-old .try{width:96px;font-size:88px}
            .plate-old.plate-small .try{width:45px;font-family:"方正姚体";font-size:50px}
            .plate-old .middle5{width:56px}
            .plate-old .dot5{width:24px}
            .plate-old .edgelong{width:16px}
            .plate-old .middle4{width:64px}
            .plate-old .dot4{width:32px}
            .plate-old.plate-small .edgelong{width:10px}
            .plate-old.plate-small .middle4{width:40px}
            .plate-old.plate-small .dot4{width:20px}
          </style>
          <path d="${backgroundPath}" fill="${backgroundColor}"/>
          <foreignObject x="0" y="0" width="${width}" height="${height}">
            <div xmlns="http://www.w3.org/1999/xhtml" class="plate${info.small ? " plate-small" : ""}${info.old ? " plate-old" : ""}" style="color: ${textColor}">
              ${info.electric ? '<div class="elec">电&#160;&#160;&#160;动</div>' : ""}
              ${info.antique ? '<div class="elec">古&#160;&#160;&#160;董</div>' : ""}
              ${info.autopilot ? '<div class="elec">自&#160;&#160;&#160;驾</div>' : ""}
              ${info.wheelchair ? '<div class="chair">♿</div>' : ""}
              ${info.num}
            </div>
          </foreignObject>
        </svg>
      `.trim().replace(/\r?\n\s*/g, "")
      ctx.logger.debug("markup", r)
      if (debug) return h.text(r)

      for (let i = 0; i < fontDataURLs.length; i++) r = r.replace(`@@_FONT${i}_@@`, await fontDataURLs[i])
      // We patched CanvasService.loadImage to take an additional `type` parameter
      const svg = ctx.canvas.loadImage(Buffer.from(r), "image/svg+xml")
      const img = await ctx.canvas.render(width * zoom, height * zoom, async cctx => {
        if (backdrop) {
          cctx.fillStyle = backdrop
          cctx.fillRect(0, 0, width * zoom, height * zoom)
        }
        cctx.drawImage(await svg, 0, 0, width * zoom, height * zoom)
      })

      const footer = info.warnings?.map(w => h("p", "警告：" + w)) ?? []
      return [img, ...footer]
    })
}
